﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel.Composition.Hosting;
using System.Configuration;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Xml.Linq;
using System.Web.Hosting;

using SBPweb.RIA.Shared.Contracts.Components;

using SBPweb.PROvibe.Server.Library;
using SBPweb.PROvibe.Server.Library.Events;
using SBPweb.PROvibe.Server.Library.Modularity;

using SBPweb.PROvibe.Shared.Library.Components;
using SBPweb.PROvibe.Server.Library.Contracts;

using ICSharpCode.SharpZipLib.Zip;

namespace SBPweb.PROvibe.Server.Core.Modularity
{

    public class ModuleManager : InitializableBase, IModuleManager
    {

        #region Fields

        private CompositionContainer compositionContainer;
        private Dictionary<Tuple<string, Version>, IModule> moduleCollection;
        private ObservableCollection<IModule> moduleList;

        #endregion

        #region Constructor logic

        internal ModuleManager()
        {
            moduleCollection = new Dictionary<Tuple<string, Version>, IModule>();
            moduleList = new ObservableCollection<IModule>();
            Modules = new ReadOnlyObservableCollection<IModule>(moduleList);
        }

        #endregion

        #region Properties

        public ReadOnlyObservableCollection<IModule> Modules
        {
            get;
            private set;
        }

        #endregion

        #region Methods

        protected override void OnInitialize()
        {
            LoadModulesFromServerFolder();
            AssignServerContextToModules();
            LoadModulesFromClientFolder();
            InitializeModules();
        }

        private void LoadModulesFromServerFolder()
        {
            LoadModulesFromFolder(new DirectoryInfo(HostingEnvironment.ApplicationPhysicalPath + ConfigurationManager.AppSettings[Constants.AppSettingsKeys.ModulesServerDirectory]));
        }

        private void LoadModulesFromFolder(DirectoryInfo directory)
        {
            AggregateCatalog aggregatorCatalog = new AggregateCatalog(new AssemblyCatalog(Assembly.GetExecutingAssembly()));

            if (!directory.Exists)
            {
                throw new IOException(string.Format("Directory at path '{0}' is not accessible!", directory.FullName));
            }

            AppDomain.CurrentDomain.AppendPrivatePath(directory.FullName);

            aggregatorCatalog.Catalogs.Add(new DirectoryCatalog(directory.FullName));
            LogManager.Logger.WriteVerbose(this, string.Format("Directory '{0}' was added to be watched for modules", directory.FullName), LogManager.Categories.Server.Modules);

            compositionContainer = new CompositionContainer(aggregatorCatalog);

            try
            {
                var modules = compositionContainer.GetExportedValues<IModule>().ToList();

                CoreModule coreModule = new CoreModule();
                modules.Add(coreModule);

                foreach (var module in modules)
                {
                    var key = Tuple.Create<string, Version>(module.Name, module.Version);

                    lock (moduleCollection)
                    {
                        if (!moduleCollection.Keys.Contains(key))
                        {
                            moduleCollection.Add(key, module);
                            moduleList.Add(module);

                            LogManager.Logger.WriteVerbose(this, string.Format("Module '{0}', Version: {1} was loaded", module.Name, module.Version), LogManager.Categories.Server.Modules);
                        }
                        else
                        {
                            LogManager.Logger.WriteVerbose(this, string.Format("Module '{0}', Version: {1} was skiped, because it's already loaded", module.Name, module.Version), LogManager.Categories.Server.Modules);
                        }
                    }
                }
            }
            catch (ReflectionTypeLoadException ex)
            {
                StringBuilder loaderExceptions = new StringBuilder();

                foreach (var exElement in ex.LoaderExceptions)
                {
                    loaderExceptions.AppendLine(exElement.ToString());
                }
                LogManager.Logger.WriteError(this, string.Format("Failed to load a module. Exception: {0}", loaderExceptions), LogManager.Categories.Server.Modules);
            }
            catch (FileLoadException ex)
            {
                LogManager.Logger.WriteError(this, string.Format("Failed to load a module. Exception: {0}", ex), LogManager.Categories.Server.Modules);
            }
            catch (Exception ex)
            {
                LogManager.Logger.WriteError(this, string.Format("Failed to load a module. Exception: {0}", ex), LogManager.Categories.Server.Modules);
            }
        }

        private void LoadModulesFromClientFolder()
        {
            string relativePath = ConfigurationManager.AppSettings[Constants.AppSettingsKeys.ModulesClientDirectory];
            DirectoryInfo directory = new DirectoryInfo(HostingEnvironment.ApplicationPhysicalPath + relativePath);
            
            if (!directory.Exists)
            {
                throw new IOException(string.Format("Directory at path '{0}' is not accessible!", directory.FullName));
            }

            lock (moduleCollection)
            {
                foreach (FileInfo file in directory.GetFiles("*.xap"))
                {
                    foreach (IModule module in ExtractExportedModulesFromXap(file, string.Format("{0}/{1}", relativePath.Replace("\\", "/"), file.Name)))
                    {
                        moduleCollection.Add(new Tuple<string, Version>(module.Name, module.Version), module);
                        moduleList.Add(module);
                    }
                }
            }

        }

        private void AssignServerContextToModules()
        {
            lock (moduleCollection)
            {
                foreach (KeyValuePair<Tuple<string, Version>, IModule> kvp in moduleCollection)
                {
                    ServerModuleBase mb = kvp.Value as ServerModuleBase;
                    if (mb != null)
                    {
                        mb.AssignServerContext(ServerContext.Instance);
                    }
                    else
                    {
                        ///TODO: log this
                        //throw new NotSupportedException(string.Format("Module type '{0}' not supported!", kvp.Value.GetType()));
                    }
                }
            }
        }

        private void InitializeModules()
        {
            lock (moduleCollection)
            {
                foreach (KeyValuePair<Tuple<string, Version>, IModule> kvp in moduleCollection)
                {
                    ServerModuleBase mb = kvp.Value as ServerModuleBase;
                    if (mb != null)
                    {
                        mb.Initialize();
                    }
                }
            }
        }
        
        internal List<IModule> ExtractExportedModulesFromXap(FileInfo file, string relativePath)
        {
            List<IModule> ret = new List<IModule>();
            using (ZipFile zip = new ZipFile(file.Open(FileMode.Open, FileAccess.Read)))
            {
                string moduleAsmName = string.Empty;
                ZipEntry zeManifest = zip.GetEntry("AppManifest.xaml");
                using (Stream xapManifestStream = zip.GetInputStream(zeManifest))
                {
                    try
                    {
                        XElement deploy = XDocument.Load(xapManifestStream).Root;
                        List<XElement> parts = (from assemblyParts in deploy.Elements().Elements()
                                                select assemblyParts).ToList();
                        moduleAsmName = parts.FirstOrDefault().Attribute("Source").Value;
                    }
                    catch (Exception ex)
                    {
                        throw new NotSupportedException(string.Format("Client module at '{0}' is not a correct XAP package!", file.FullName), ex);
                    }
                }

                if (!string.IsNullOrEmpty(moduleAsmName))
                {
                    ZipEntry zeModule = zip.GetEntry(moduleAsmName);
                    using (Stream xapModuleStream = zip.GetInputStream(zeModule))
                    {
                        int size = (int)zeModule.Size;
                        byte[] bytes = new byte[size];
                        xapModuleStream.Read(bytes, 0, size);
                        xapModuleStream.Flush();

                        Assembly asm = Assembly.Load(bytes);
                        AssemblyName asmName = asm.GetName();
                        AssemblyTitleAttribute asmTitle = (AssemblyTitleAttribute)(asm.GetCustomAttributes(typeof(AssemblyTitleAttribute), false)[0]);
                        ClientModule module = new ClientModule(-1, asmTitle.Title, asmName.Version, relativePath);
                        ret.Add(module);
                    }                
                }
                
            }

            return ret;
        }

        protected override void OnTerminate()
        {
        }

        #endregion

    }

}